package de.uni_bremen.exceptions.oops;

import java.util.HashMap;

/**
 * Die Klasse repräsentiert eine Klassendeklaration im Syntaxbaum. Zudem stellt sie Methoden zum Typvergleich zur
 * Verfügung.
 */
class ClassDeclaration extends Declaration
{
	enum AccessModificator
	{
		PUBLIC, PROTECTED, PRIVATE
	}
	
	/**
	 * Konstante für die Größe der Verwaltungsinformation am Anfang eines jeden Objekts. Bisher ist die Größe 0.
	 */
	static final int								HEADERSIZE		= 1;
	
	/** Ein interner Typ für das Ergebnis von Methoden. */
	static final ClassDeclaration					voidType		= new ClassDeclaration(
																			new Identifier("_Void", null));
	
	/** Ein interner Typ für null. Dieser Typ ist kompatibel zu allen Klassen. */
	static final ClassDeclaration					nullType		= new ClassDeclaration(
																			new Identifier("_Null", null));
	
	/** Der interne Basisdatentyp für Zahlen. */
	static final ClassDeclaration					intType			= new ClassDeclaration(new Identifier("_Integer",
																			null));
	
	/** Der interne Basisdatentyp für Wahrheitswerte. */
	static final ClassDeclaration					boolType		= new ClassDeclaration(new Identifier("_Boolean",
																			null));
	
	/** Die Klasse Integer. */
	static final ClassDeclaration					objectClass		= new ClassDeclaration(new Identifier("Object",
																			null));
	
	/** Die Klasse Integer. */
	static final ClassDeclaration					intClass		= new ClassDeclaration(new Identifier("Integer",
																			null));
	
	/** Die Klasse Boolean. */
	static final ClassDeclaration					boolClass		= new ClassDeclaration(new Identifier("Boolean",
																			null));
	
	/** Die Klasse Exception. */
	static final ExceptionClassDeclaration			exceptionClass	= new ExceptionClassDeclaration();
	
	/** Die Attribute dieser Klasse. */
	HashMap<VarDeclaration, AccessModificator>		attributes		= new HashMap<VarDeclaration, ClassDeclaration.AccessModificator>();
	
	/** Die Methoden dieser Klasse. */
	HashMap<MethodDeclaration, AccessModificator>	methods			= new HashMap<MethodDeclaration, ClassDeclaration.AccessModificator>();
	
	/** Aktueller Methodenindex für die VMT */
	int												vmt_index		= -1;
	
	/**
	 * TODO
	 * 
	 * @throws CompileException
	 *             TODO
	 */
	int getVMTIndex() throws CompileException
	{
		if (vmt_index == -1)
			generateVMTIndex();
		return vmt_index;
	}
	
	/**
	 * TODO
	 * 
	 * @throws CompileException
	 *             TODO
	 */
	private void generateVMTIndex() throws CompileException
	{
		if (this == objectClass)
			vmt_index = 0;
		else
			vmt_index = baseClass.getVMTIndex();
		for (MethodDeclaration m : methods.keySet())
		{
			int i = this != objectClass ? baseClass.getMethodIndex(m) : -1;
			m.index = i != -1 ? i : vmt_index++;
		}
	}
	
	/**
	 * TODO
	 * 
	 * @param methoddecl
	 *            TODO
	 * @throws CompileException
	 *             TODO
	 */
	int getMethodIndex(MethodDeclaration methoddecl) throws CompileException
	{
		int override = -1;
		for (MethodDeclaration m : methods.keySet())
			if (m.identifier.equals(methoddecl.identifier))
				if (m.equals(methoddecl))
					override = m.index;
				else
					throw new CompileException("Überladungen sind nicht unterstützt", m.identifier.position);
		// weiter nach oben suchen
		if (override == -1 && this != objectClass)
			override = baseClass.getMethodIndex(methoddecl);
		if (override != -1)
			return override;
		return -1;
	}
	
	/** TODO */
	MethodDeclaration[]	vmt;
	
	/**
	 * TODO
	 * 
	 * @param vmt
	 *            TODO
	 */
	void fillVMT(MethodDeclaration[] vmt)
	{
		if (this != objectClass)
			baseClass.fillVMT(vmt);
		for (MethodDeclaration m : methods.keySet())
			vmt[m.index] = m;
	}
	
	/** Die innerhalb dieser Klasse sichtbaren Deklarationen. */
	Declarations			declarations;
	
	/**
	 * Die Größe eines Objekts dieser Klasse. Die Größe wird innerhalb von {@link #resolveSignatures(Declarations)
	 * resolveSignatures} bestimmt.
	 */
	int						objectSize	= -1;
	
	/** TODO */
	ResolvableIdentifier	baseIdent;
	
	/** TODO */
	ClassDeclaration		baseClass	= objectClass;
	
	/**
	 * Konstruktor.
	 * 
	 * @param name
	 *            Der Name der deklarierten Klasse
	 */
	ClassDeclaration(Identifier name)
	{
		super(name);
	}
	
	/**
	 * TODO
	 * 
	 * @param declarations
	 *            TODO
	 * @throws CompileException
	 *             TODO
	 */
	private int resolveObjectSize(Declarations declarations) throws CompileException
	{
		if (objectSize != -1)
			return objectSize;
		// Standardgröße für Objekte festlegen
		if (this == objectClass)
			objectSize = HEADERSIZE;
		else
			objectSize = baseClass.resolveObjectSize(declarations) + 1;
		// Attributtypen auflösen und Indizes innerhalb des Objekts vergeben
		for (VarDeclaration a : attributes.keySet())
		{
			a.contextAnalysis(declarations);
			a.offset = objectSize++;
		}
		return objectSize;
	}
	
	/**
	 * Die Methode führt die Kontextanalyse für diese Klassen-Deklaration durch.
	 * 
	 * @param declarations
	 *            Die an dieser Stelle gültigen Deklarationen.
	 * @throws CompileException
	 *             Während der Kontextanylyse wurde ein Fehler gefunden.
	 */
	void contextAnalysis(Declarations declarations) throws CompileException
	{
		contextAnalysis(declarations, true);
	}
	
	/**
	 * Die Methode führt die Kontextanalyse für diese Klassen-Deklaration durch.
	 * 
	 * @param declarations
	 *            Die an dieser Stelle gültigen Deklarationen.
	 * @param firstPass
	 *            true: Wendet die Kontextanalyse auf die Klasse an. <br>
	 *            false: Wendet die Kontextanalyse auf die Methoden an.
	 * @throws CompileException
	 *             Während der Kontextanylyse wurde ein Fehler gefunden.
	 */
	void contextAnalysis(Declarations declarations, boolean firstPass) throws CompileException
	{
		if (firstPass)
		{
			resolveObjectSize(declarations);
			
			// Neuen Deklarationsraum schaffen
			// if (this != objectClass && false)
			// {
			// if (baseClass.declarations == null)
			// baseClass.contextAnalysis(declarations, true);
			// declarations = (Declarations) baseClass.declarations.clone();
			// }
			// else
			declarations.enter();
			declarations.currentClass = this;
			
			// Attribute eintragen
			for (VarDeclaration a : attributes.keySet())
			{
				declarations.add(a);
			}
			
			// Methoden eintragen
			for (MethodDeclaration m : methods.keySet())
			{
				declarations.add(m);
			}
			
			// Wird auf ein Objekt dieser Klasse zugegriffen, werden die Deklarationen
			// in diesem Zustand benötigt. Deshalb werden sie in der Klasse gespeichert.
			this.declarations = (Declarations) declarations.clone();
			
			// Deklarationsraum verlassen
			declarations.leave();
			
			// Typanalyse für Methoden durchführen
			for (MethodDeclaration m : methods.keySet())
			{
				m.contextAnalysis((Declarations) this.declarations.clone(), true);
			}
		}
		else
		{
			// VMT generieren
			vmt = new MethodDeclaration[getVMTIndex()];
			fillVMT(vmt);
			
			// Kontextanalyse für Methoden durchführen
			for (MethodDeclaration m : methods.keySet())
			{
				m.contextAnalysis((Declarations) this.declarations.clone(), false);
			}
		}
	}
	
	/**
	 * Die Methode prüft, ob dieser Typ kompatibel mit einem anderen Typ ist.
	 * 
	 * @param expected
	 *            Der Typ, mit dem verglichen wird.
	 * @return Sind die beiden Typen kompatibel?
	 */
	boolean isA(ClassDeclaration expected)
	{
		// Spezialbehandlung für null, das mit allen Klassen kompatibel ist,
		// aber nicht mit den Basisdatentypen _Integer und _Boolean sowie auch nicht
		// an Stellen erlaubt ist, wo gar kein Wert erwartet wird.
		if (this == nullType && expected != intType && expected != boolType && expected != voidType)
		{
			return true;
		}
		else
		{
			return this == expected || this == nullType && expected.isA(objectClass) || this != objectClass
					&& baseClass != null && baseClass.isA(expected);
		}
	}
	
	/**
	 * Die Methode erzeugt eine Ausnahme für einen Typfehler. Sie wandelt dabei intern verwendete Typnamen in die auch
	 * außen sichtbaren Namen um.
	 * 
	 * @param expected
	 *            Der Typ, der nicht kompatibel ist.
	 * @param position
	 *            Die Stelle im Quelltext, an der der Typfehler gefunden wurde.
	 * @throws CompileException
	 *             Die Meldung über den Typfehler.
	 */
	static void typeError(ClassDeclaration expected, Position position) throws CompileException
	{
		if (expected == intType)
		{
			throw new CompileException("Ausdruck vom Typ Integer erwartet", position);
		}
		else
			if (expected == boolType)
			{
				throw new CompileException("Ausdruck vom Typ Boolean erwartet", position);
			}
			else
				if (expected == ClassDeclaration.voidType)
				{
					throw new CompileException("Hier darf keinen Wert zurückgeliefert werden", position);
				}
				else
				{
					throw new CompileException("Ausdruck vom Typ " + expected.identifier.name + " erwartet", position);
				}
	}
	
	/**
	 * Die Methode prüft, ob dieser Typ kompatibel mit einem anderen Typ ist. Sollte das nicht der Fall sein, wird eine
	 * Ausnahme mit einer Fehlermeldung generiert.
	 * 
	 * @param expected
	 *            Der Typ, mit dem verglichen wird.
	 * @param position
	 *            Die Position im Quelltext, an der diese Überprüfung relevant ist. Die Position wird in der
	 *            Fehlermeldung verwendet.
	 * @throws CompileException
	 *             Die Typen sind nicht kompatibel.
	 */
	void check(ClassDeclaration expected, Position position) throws CompileException
	{
		if (!isA(expected))
		{
			typeError(expected, position);
		}
	}
	
	/**
	 * Die Methode gibt diese Deklaration in einer Baumstruktur aus.
	 * 
	 * @param tree
	 *            Der Strom, in den die Ausgabe erfolgt.
	 */
	void print(TreeStream tree)
	{
		tree.println("CLASS " + identifier.name);
		// if (baseClass != null)
		tree.indent();
		if (!attributes.isEmpty())
		{
			tree.println("ATTRIBUTES");
			tree.indent();
			for (VarDeclaration a : attributes.keySet())
			{
				tree.println(attributes.get(a) + ":");
				tree.indent();
				a.print(tree);
				tree.unindent();
			}
			tree.unindent();
		}
		if (!methods.isEmpty())
		{
			tree.println("METHODS");
			tree.indent();
			for (MethodDeclaration m : methods.keySet())
			{
				tree.println(methods.get(m) + ":");
				tree.indent();
				m.print(tree);
				tree.unindent();
			}
			tree.unindent();
		}
		tree.unindent();
	}
	
	/**
	 * Generiert den Assembler-Code für diese Klasse. Dabei wird davon ausgegangen, dass die Kontextanalyse vorher
	 * erfolgreich abgeschlossen wurde.
	 * 
	 * @param code
	 *            Der Strom, in den die Ausgabe erfolgt.
	 */
	void generateCode(CodeStream code)
	{
		code.println("; CLASS " + identifier.name);
		code.println(identifier.name + ":");
		for (MethodDeclaration m : vmt)
			code.println("DAT 1, " + identifier.name + "_" + m.identifier.name);
		// Synthese für alle Methoden
		for (MethodDeclaration m : methods.keySet())
		{
			m.generateCode(code);
		}
		code.println("; END CLASS " + identifier.name);
	}
	
	/**
	 * Die Methode gibt den Namen dieser Klasse in Textform aus.
	 */
	@Override
	public String toString()
	{
		return "CLASS " + identifier.name;
	}
}
